package net.butfly.bus.client;

import java.io.InputStream;
import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

import net.butfly.albacore.exception.SystemException;
import net.butfly.albacore.facade.Facade;
import net.butfly.bus.Constants;
import net.butfly.bus.Constants.FilterSide;
import net.butfly.bus.Request;
import net.butfly.bus.Response;
import net.butfly.bus.TX;
import net.butfly.bus.client.config.ClientConfig;
import net.butfly.bus.client.config.XMLConfigClientParser;
import net.butfly.bus.client.connection.ConnectionFactory;
import net.butfly.bus.config.ConfigLoader;
import net.butfly.bus.config.ConfigParser;
import net.butfly.bus.config.loader.ClasspathConfigLoad;
import net.butfly.bus.context.Context;
import net.butfly.bus.facade.InternalFacade;
import net.butfly.bus.filter.Filter;
import net.butfly.bus.filter.FilterBase;
import net.butfly.bus.filter.FilterChain;
import net.butfly.bus.impl.Connection;
import net.butfly.bus.router.Router;
import net.butfly.bus.util.BusUtils;
import net.butfly.bus.util.TXUtils;

public class Client implements InternalFacade {
	private static final long serialVersionUID = 8335891556908061417L;

	private Map<String, Serializable> context;
	private FilterChain chain;
	private Router router;

	protected ConfigLoader loader;
	protected ConfigParser parser;

	private ClientConfig config;

	// private ClientInvoker invoker;

	/**
	 * @param configURL
	 *            Supportting: <br>
	 *            <h1>standand urls</h1><br>
	 *            http://...<br>
	 *            ftp://...<br>
	 *            file://...<br>
	 *            <h1>java urls</h1><br>
	 *            classpath://...<br>
	 *            jdbc://...<br>
	 *            <h1>bus urls</h1><br>
	 *            albus://...<br>
	 *            <h3>and so on...</h3><br>
	 */
	public Client(String configURL) {
		this(configURL, null);
	}

	public Client(String configLocation, Map<String, Serializable> context) {
		this.context = context;
		this.initialize(configLocation);
		this.config = this.parser.parse();
		this.router = new DefaultRouter(this.config);
		this.chain = new FilterChain(config.getFilterBeanList(), client, FilterSide.CLIENT);
		this.internal = this.getService(InternalFacade.class);
	}

	public <F extends Facade> F getService(Class<F> facadeClass) {
		return getService(facadeClass, null);
	}

	@SuppressWarnings("unchecked")
	public <F extends Facade> F getService(Class<F> facadeClass, Map<String, Serializable> context) {
		return (F) Proxy.newProxyInstance(facadeClass.getClassLoader(), new Class<?>[] { facadeClass }, new ServiceProxy(
				generateContext(context)));
	}

	public <T> T execute(String code, Object... args) {
		return this.execute(code, TX.INIT_VERSION, args);
	}

	public <T> T execute(String code, String version, Object... args) {
		return this.execute(code, version, null, args);
	}

	public <T> T execute(String code, Map<String, Serializable> context, Object... args) {
		return this.execute(code, TX.INIT_VERSION, context, args);
	}

	@SuppressWarnings("unchecked")
	public <T> T execute(String code, String version, Map<String, Serializable> context, Object... args) {
		return (T) this.execute(new Request(TXUtils.TXImpl(code, version), args, this.generateContext(context)));
	}

	public Map<String, Serializable> generateContext(Map<String, Serializable> context) {
		return this.generateContext(context, Context.Mode.MergingWithNew);
	}

	public Map<String, Serializable> generateContext(Map<String, Serializable> context, Context.Mode mode) {
		Map<String, Serializable> ctx = new HashMap<String, Serializable>();
		Context.merge(ctx, this.context);
		Context.merge(ctx, context, mode);
		return ctx;
	}

	public Response execute(Request request) {
		if (request == null)
			throw new SystemException(Constants.UserError.BAD_REQUEST, "Request null invalid.");
		if (request.code() == null || "".equals(request.code().trim()))
			throw new SystemException(Constants.UserError.BAD_REQUEST, "Request empty tx code invalid.");
		if (request.version() == null || "".equals(request.version().trim()))
			throw new SystemException(Constants.UserError.BAD_REQUEST, "Request empty tx version invalid.");
		Response response = new Response();
		try {
			chain.execute(request, response);
		} catch (Exception ex) {
			throw new SystemException("", ex);
		}
		return response;
	}

	private class ServiceProxy implements InvocationHandler {
		private Map<String, Serializable> context = new HashMap<String, Serializable>();

		public ServiceProxy(Map<String, Serializable> context) {
			this.context = context;
		}

		public Object invoke(Object obj, Method method, Object[] args) {
			TX tx = method.getAnnotation(TX.class);
			if (null != tx) {
				Request request = new Request(tx.value(), tx.version(), args);
				request.context(context);
				Response response = Client.this.execute(request);
				// confirm exception processing on exception handler filter
				// apply for server but not client side.
				BusUtils.errorToException(response);
				return response.getResult();
			} else
				throw new SystemException(Constants.UserError.TX_NOT_EXIST, "Request tx code not found on method ["
						+ method.toString() + "].");

		}
	}

	protected final Filter client = new FilterBase() {
		@Override
		public void execute(Request request, Response response) throws Exception {
			String nodeId = router.route(request.code(), request.version());
			if (nodeId == null)
				throw new SystemException(Constants.SystemError.ROUTE_NOT_FOUND, "Client can not find node for ["
						+ request.code() + ":" + request.version() + "] in [" + Client.this.loader.getConfigLocation() + "].");

			logger.info(Context.getFlowNo() + " route to node " + nodeId + ".");
			Connection connection = ConnectionFactory.getConnection(config.getNodeBean(nodeId));
			if (connection == null) {
				throw new SystemException(Constants.UserError.NODE_NOT_FOUND, "Cannot found connection for node " + nodeId
						+ ", please check node segments in server config.");
			}
			logger.info(Context.getFlowNo() + " begin remote bus Server request.");
			response.copyFrom(connection.send(request));
		}
	};

	private InternalFacade internal;

	@Override
	public String echo(String echo) {
		return internal.echo(echo);
	}

	@Override
	public boolean isDebug() {
		return false;
	}

	/**
	 * @param configLocation
	 * 
	 * @TODO: enable multiply configuration styles.
	 * @TODO: check debug to determinate whether to load internal configuration.
	 */
	private void initialize(String configLocation) {
		this.loader = new ClasspathConfigLoad(configLocation);
		InputStream is = this.loader.load();
		// load local / internal config
		if (null == is) {
			this.loader = new ClasspathConfigLoad(Constants.Configuration.DEFAULT_CLIENT_CONFIG);
			is = this.loader.load();
			if (null == is) {
				this.loader = new ClasspathConfigLoad(Constants.Configuration.INTERNAL_CLIENT_CONFIG);
				is = this.loader.load();
			}
		}
		if (null == is)
			throw new SystemException(Constants.UserError.CONFIG_ERROR, "Bus configurations invalid: " + configLocation);
		this.parser = new XMLConfigClientParser(this.loader.load());
	}
}
